<?php

declare(strict_types=1);

namespace Common;

use App\Exception\BusinessException;
use App\Http\Common\ResultCode;
use GuzzleHttp\Client;
use GuzzleHttp\Exception\GuzzleException;
use GuzzleHttp\Exception\RequestException;
use GuzzleHttp\RequestOptions;
use Hyperf\Guzzle\ClientFactory;
use Psr\Http\Message\ResponseInterface;

class HttpClient
{
    public const RESPONSE_RAW = 'raw';
    public const RESPONSE_ARRAY = 'array';
    public const RESPONSE_OBJECT = 'object';

    protected Client $client;

    protected array $config = [];

    protected string $responseType = self::RESPONSE_ARRAY;

    public function __construct(array $config = [])
    {
        $this->config = $config;
        $this->client = Common::container()->get(ClientFactory::class)->create($config);
    }

    /**
     * 发送GET请求
     *
     * @param string $uri 请求地址
     * @param array $options 请求选项
     * @return array|ResponseInterface|object
     * @throws GuzzleException
     */
    public function get(string $uri, array $options = [])
    {
        return $this->request('GET', $uri, $options);
    }

    /**
     * 发送HTTP请求
     *
     * @param string $method HTTP方法 (GET, POST, PUT, DELETE等)
     * @param string $uri 请求地址
     * @param array $options 请求选项
     * @param string|null $contentType Content-Type类型
     * @return array|ResponseInterface|object
     * @throws GuzzleException
     */
    public function request(string $method, string $uri, array $options = [], ?string $contentType = null)
    {
        try {
            if ($contentType) {
                $options['headers']['Content-Type'] = $contentType;
            }

            $response = $this->client->request($method, $uri, $options);
            $this->closeMultipartStreams($options);
            return $this->formatResponse($response);
        } catch (RequestException $e) {
            $this->closeMultipartStreams($options);
            $message = "HTTP请求失败";
            if ($e->hasResponse()) {
                $response = $e->getResponse();
                $message .= " (HTTP " . $response->getStatusCode() . "): " . $response->getReasonPhrase();
                $body = (string)$response->getBody();
                if (!empty($body)) {
                    $message .= " - 响应内容: " . $body;
                }
            } else {
                Common::logException(__CLASS__, $e, 'http 请求异常');
                throw $e;
            }
            Common::logException(__CLASS__, $e, $message);
            return ['error' => $response->getStatusCode() ?? 0];
        } catch (\Exception $e) {
            $this->closeMultipartStreams($options);
            Common::logException(__CLASS__, $e, 'http 请求异常');
            throw new BusinessException(ResultCode::FAIL, $e->getMessage());
        }
    }

    /**
     * 关闭multipart中的文件流资源
     *
     * @param array $options 请求选项
     */
    protected function closeMultipartStreams(array $options): void
    {
        if (isset($options['multipart']) && is_array($options['multipart'])) {
            foreach ($options['multipart'] as $part) {
                if (isset($part['contents']) && is_resource($part['contents'])) {
                    // 检查资源是否仍然有效
                    if (get_resource_type($part['contents']) !== 'Unknown') {
                        fclose($part['contents']);
                    }
                }
            }
        }
    }

    /**
     * 格式化响应结果
     *
     * @param ResponseInterface $response 响应对象
     * @return mixed
     */
    protected function formatResponse(ResponseInterface $response)
    {
        // 根据设置的响应类型决定返回格式
        if ($this->responseType === self::RESPONSE_RAW) {
            return $response;
        }

        $contentType = $response->getHeaderLine('Content-Type');

        // 如果是JSON响应，自动解析
        if (strpos($contentType, 'application/json') !== false) {
            $body = (string)$response->getBody();
            $data = json_decode($body, true);

            if ($this->responseType === self::RESPONSE_OBJECT) {
                return json_decode($body);
            }

            return ['content' => is_array($data) ? $data : $body];
        }

        // 如果是XML响应
        if (
            strpos($contentType, 'application/xml') !== false ||
            strpos($contentType, 'text/xml') !== false
        ) {
            if ($this->responseType === self::RESPONSE_OBJECT) {
                $body = (string)$response->getBody();
                return simplexml_load_string($body);
            }
            return ['content' => (string)$response->getBody()];
        }

        // 如果是文本类型
        if (strpos($contentType, 'text/') !== false) {
            return (string)$response->getBody();
        }

        // 其他情况根据响应类型决定
        if ($this->responseType === self::RESPONSE_ARRAY) {
            // 尝试解析为数组
            $body = (string)$response->getBody();
            if (!empty($body)) {
                return ['content' => $body];
            }
        } else if ($this->responseType === self::RESPONSE_OBJECT) {
            // 尝试解析为对象
            $body = (string)$response->getBody();
            if (!empty($body)) {
                return (object)['content' => $body];
            }
        }

        // 默认返回原始响应对象
        return $response;
    }

    /**
     * 发送DELETE请求
     *
     * @param string $uri 请求地址
     * @param array $data 请求数据
     * @param string|null $contentType Content-Type类型
     * @param array $options 请求选项
     * @return array|ResponseInterface|object
     * @throws GuzzleException
     */
    public function delete(string $uri, array $data = [], ?string $contentType = null, array $options = [])
    {
        $options = $this->prepareDataOptions($data, $options, $contentType);
        return $this->request('DELETE', $uri, $options, $contentType);
    }

    /**
     * 准备请求数据选项
     *
     * @param array $data 请求数据
     * @param array $options 请求选项
     * @param string|null $contentType Content-Type类型
     * @return array
     */
    protected function prepareDataOptions(array $data, array $options, ?string $contentType): array
    {
        if (!empty($data)) {
            if ($contentType === 'application/json') {
                $options[RequestOptions::JSON] = $data;
            } elseif ($contentType === 'multipart/form-data') {
                $options[RequestOptions::FORM_PARAMS] = $data;
            } else {
                $options[RequestOptions::BODY] = is_array($data) ? http_build_query($data) : $data;
            }
        }

        return $options;
    }

    /**
     * 发送JSON格式的POST请求
     *
     * @param string $uri 请求地址
     * @param array $data 请求数据
     * @param array $options 请求选项
     * @return array|ResponseInterface|object
     * @throws GuzzleException
     */
    public function postJson(string $uri, array $data = [], array $options = [])
    {
        return $this->post($uri, $data, 'application/json', $options);
    }

    /**
     * 发送POST请求
     *
     * @param string $uri 请求地址
     * @param array $data 请求数据
     * @param string|null $contentType Content-Type类型
     * @param array $options 请求选项
     * @return array|ResponseInterface|object
     * @throws GuzzleException
     */
    public function post(string $uri, array $data = [], ?string $contentType = null, array $options = [])
    {
        $options = $this->prepareDataOptions($data, $options, $contentType);
        return $this->request('POST', $uri, $options, $contentType);
    }

    /**
     * 发送JSON格式的PUT请求
     *
     * @param string $uri 请求地址
     * @param array $data 请求数据
     * @param array $options 请求选项
     * @return array|ResponseInterface|object
     * @throws GuzzleException
     */
    public function putJson(string $uri, array $data = [], array $options = [])
    {
        return $this->put($uri, $data, 'application/json', $options);
    }

    /**
     * 发送PUT请求
     *
     * @param string $uri 请求地址
     * @param array $data 请求数据
     * @param string|null $contentType Content-Type类型
     * @param array $options 请求选项
     * @return array|ResponseInterface|object
     * @throws GuzzleException
     */
    public function put(string $uri, array $data = [], ?string $contentType = null, array $options = [])
    {
        $options = $this->prepareDataOptions($data, $options, $contentType);
        return $this->request('PUT', $uri, $options, $contentType);
    }

    /**
     * 发送表单格式的POST请求
     *
     * @param string $uri 请求地址
     * @param array $data 请求数据
     * @param array $options 请求选项
     * @return array|ResponseInterface|object
     * @throws GuzzleException
     */
    public function postForm(string $uri, array $data = [], array $options = [])
    {
        return $this->post($uri, $data, 'multipart/form-data', $options);
    }

    /**
     * 分片上传文件
     *
     * @param string $uri 请求地址
     * @param string $filePath 文件路径
     * @param array $data 其他表单数据
     * @param int $chunkSize 分片大小（字节），默认为10MB
     * @param int $startIndex 开始上传的分片索引，默认为0
     * @param array $options 请求选项
     * @return array|ResponseInterface|object
     * @throws GuzzleException
     */
    public function postFileChunked(
        string $uri,
        string $filePath,
        array $data = [],
        int $chunkSize = 10485760,
        int $startIndex = 0,
        string $identifier = null,
        array $options = []
    )
    {
        if (!file_exists($filePath)) {
            throw new BusinessException(ResultCode::FAIL, "文件不存在: $filePath");
        }

        // 检查文件是否可读
        if (!is_readable($filePath)) {
            throw new BusinessException(ResultCode::FAIL, "文件不可读: $filePath");
        }

        $fileSize = filesize($filePath);
        $fileName = basename($filePath);
        $totalChunks = ceil($fileSize / $chunkSize);
        $identifier = $identifier ?? md5($filePath . $fileSize . filemtime($filePath));

        // 如果文件小于等于分片大小，直接使用普通上传
        if ($totalChunks <= 1) {
            return $this->postFile($uri, ['path' => $filePath, 'name' => $fileName], $data, $options);
        }

        // 分片上传
        $fileHandle = fopen($filePath, 'rb');
        if ($fileHandle === false) {
            throw new BusinessException(ResultCode::FAIL, "无法打开文件: $filePath");
        }

        try {
            // 移动文件指针到开始上传的位置
            if ($startIndex > 0) {
                $startPosition = $startIndex * $chunkSize;
                if (fseek($fileHandle, $startPosition) !== 0) {
                    throw new BusinessException(ResultCode::FAIL, "无法定位到指定的分片位置: $startIndex");
                }
            }

            for ($i = $startIndex; $i < $totalChunks; $i++) {
                // 读取分片数据
                $chunkData = fread($fileHandle, $chunkSize);
                if ($chunkData === false) {
                    throw new BusinessException(ResultCode::FAIL, "读取文件分片失败: $filePath");
                }

                // 创建临时分片文件
                $tempChunkPath = tempnam(sys_get_temp_dir(), 'chunk_');
                if ($tempChunkPath === false) {
                    throw new BusinessException(ResultCode::FAIL, "创建临时分片文件失败");
                }

                $tempHandle = fopen($tempChunkPath, 'wb');
                if ($tempHandle === false) {
                    throw new BusinessException(ResultCode::FAIL, "无法创建临时分片文件: $tempChunkPath");
                }

                if (fwrite($tempHandle, $chunkData) === false) {
                    fclose($tempHandle);
                    unlink($tempChunkPath);
                    throw new BusinessException(ResultCode::FAIL, "写入临时分片文件失败: $tempChunkPath");
                }
                fclose($tempHandle);

                // 准备分片上传数据
                $chunkDataArray = array_merge($data, [
                    'index' => $i,
                    'total' => $totalChunks,
                    'identifier' => $identifier,
                    'filename' => $fileName,
                ]);

                // 上传分片
                $response = $this->postFile($uri, [
                    'path' => $tempChunkPath,
                    'name' => $fileName
                ], $chunkDataArray, $options);
                // 删除临时分片文件
                unlink($tempChunkPath);

                // 如果不是最后一次上传且返回错误，则中断
                if ($i < $totalChunks - 1) {
                    $responseArray = (array)$response;
                    if (isset($responseArray['error'])) {
                        throw new BusinessException(ResultCode::FAIL, "分片上传失败: 分片" . ($i + 1));
                    }
                }

                // 如果是最后一次上传，返回最终响应
                if ($i == $totalChunks - 1) {
                    return $response;
                }
            }
        } catch (\Throwable $e) {
            Common::logException(__CLASS__, $e);
            throw new BusinessException(ResultCode::FAIL, "分片上传失败");
        } finally {
            fclose($fileHandle);
        }

        throw new BusinessException(ResultCode::FAIL, "分片上传未完成");
    }

    /**
     * 上传文件
     *
     * @param string $uri 请求地址
     * @param array $files 要上传的文件数组，格式: ['fieldname' => '/path/to/file']
     * @param array $data 其他表单数据
     * @param array $options 请求选项
     * @return array|ResponseInterface|object
     * @throws GuzzleException
     */
    public function postFile(string $uri, array $file = [], array $data = [], array $options = [])
    {
        $multipart = [];
        $fileResources = [];
        try {
            // 添加文件
            $filePath = $file['path'];
            $fileName = $file['name'] ?? basename($file['path']);
            if (!file_exists($filePath)) {
                throw new BusinessException(ResultCode::FAIL, "文件不存在: $filePath");
            }

            // 检查文件是否可读
            if (!is_readable($filePath)) {
                throw new BusinessException(ResultCode::FAIL, "文件不可读: $filePath");
            }

            $fileResource = fopen($filePath, 'r');
            if ($fileResource === false) {
                throw new BusinessException(ResultCode::FAIL, "无法打开文件: $filePath");
            }

            // 保存文件资源引用以便在出错时关闭
            $fileResources[] = $fileResource;

            $multipart[] = [
                'name' => 'file',
                'contents' => $fileResource,
                'filename' => $fileName
            ];


            // 添加其他表单数据
            foreach ($data as $key => $value) {
                $multipart[] = [
                    'name' => $key,
                    'contents' => $value
                ];
            }

            $options['multipart'] = $multipart;
            return $this->request('POST', $uri, $options);
        } catch (\Exception $e) {
            // 如果发生异常，确保所有已打开的文件资源都被关闭
            foreach ($fileResources as $resource) {
                if (is_resource($resource)) {
                    fclose($resource);
                }
            }
            throw $e;
        }
    }

    /**
     * 设置基础URI
     *
     * @param string $baseUri 基础URI
     * @return self
     */
    public function setBaseUri(string $baseUri): self
    {
        return $this->configure(['base_uri' => $baseUri]);
    }

    /**
     * 重新配置客户端
     *
     * @param array $config 客户端配置
     * @return self
     */
    public function configure(array $config): self
    {
        $this->config = array_merge($this->config, $config);
        $this->client = new Client($this->config);
        return $this;
    }

    /**
     * 设置超时时间
     *
     * @param int $timeout 超时时间（秒）
     * @return self
     */
    public function setTimeout(int $timeout): self
    {
        return $this->configure(['timeout' => $timeout]);
    }

    /**
     * 获取客户端配置
     *
     * @return array
     */
    public function getConfig(): array
    {
        return $this->config;
    }

    /**
     * 下载文件
     *
     * @param string $save 文件保存路径
     * @param string $uri 请求地址
     * @param array $data 请求数据
     * @param array $options 请求选项
     * @return string 下载的文件名
     * @throws GuzzleException
     */
    public function downloadFile(string $save, string $uri, array $data = [], array $options = []): string
    {
        // 设置响应类型为原始响应对象以便获取响应头
        $originalResponseType = $this->responseType;
        $this->setResponseType(self::RESPONSE_RAW);

        $options = $this->prepareDataOptions($data, $options, 'application/x-www-form-urlencoded');
        $response = $this->request('GET', $uri, array_merge($options, ['sink' => $save]));

        // 恢复原来的响应类型
        $this->setResponseType($originalResponseType);

        // 从响应头中获取真实文件名
        $realFilename = $this->getFilenameFromResponse($response, $save);

        return $realFilename;
    }

    /**
     * 设置响应类型
     *
     * @param string $type 响应类型 (raw|array|object)
     * @return self
     */
    public function setResponseType(string $type): self
    {
        $this->responseType = $type;
        return $this;
    }

    /**
     * 从响应中提取文件名
     *
     * @param ResponseInterface $response 响应对象
     * @param string $savePath 保存路径
     * @return string 文件名
     */
    protected function getFilenameFromResponse(ResponseInterface $response, string $savePath): string
    {
        // 首先尝试从 Content-Disposition 头获取文件名
        $contentDisposition = $response->getHeaderLine('Content-Disposition');
        if (!empty($contentDisposition)) {
            // 匹配 filename=\"filename.ext\" 或 filename=filename.ext
            if (preg_match('/filename[^;=\n]*=(([\'\"]).*?\2|[^;\n]*)/i', $contentDisposition, $matches)) {
                $filename = trim($matches[1], '"\'');
                if (!empty($filename)) {
                    return $filename;
                }
            }
        }

        // 如果 Content-Disposition 中没有文件名，则从保存路径获取
        return basename($savePath);
    }
}